enhancement(cli): 优化了 CLI 插件列表的对比与合并逻辑#8705
Conversation
…formance perf: optimize plugin list merge
…formance-8i77l2 Fix plugin list building and status resolution; add unit tests
…formance-wsd49n Use `is_dir()` for plugin path check and add unit tests for `build_plug_list`
There was a problem hiding this comment.
Code Review
This pull request refactors the plugin listing utility in astrbot/cli/utils/plugin.py to use iterdir() for directory traversal and optimizes the local-to-online plugin comparison logic using dictionary and set lookups. It also introduces unit tests for these changes. The review feedback suggests replacing plugins_dir.exists() with plugins_dir.is_dir() to prevent a potential NotADirectoryError when a file path is provided, and recommends deleting the redundant tests/test_cli_plugin_utils.py file as its contents are duplicated in the tests/unit directory.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2f335f8c4a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Hey - I've found 4 issues, and left some high level feedback:
build_plug_listnow callsplugins_dir.iterdir()unconditionally when the path exists; ifplugins_diris a file rather than a directory this will raiseNotADirectoryError, so consider early-returning or skipping iteration whennot plugins_dir.is_dir()to match the new test’s intent of treating file paths as an empty local set.- There appear to be two very similar test modules (
tests/unit/test_cli_plugin_utils.pyandtests/test_cli_plugin_utils.py) defining the same helpers and first test; consider consolidating them into a single file to avoid duplication and potential divergence.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- `build_plug_list` now calls `plugins_dir.iterdir()` unconditionally when the path exists; if `plugins_dir` is a file rather than a directory this will raise `NotADirectoryError`, so consider early-returning or skipping iteration when `not plugins_dir.is_dir()` to match the new test’s intent of treating file paths as an empty local set.
- There appear to be two very similar test modules (`tests/unit/test_cli_plugin_utils.py` and `tests/test_cli_plugin_utils.py`) defining the same helpers and first test; consider consolidating them into a single file to avoid duplication and potential divergence.
## Individual Comments
### Comment 1
<location path="astrbot/cli/utils/plugin.py" line_range="168-177" />
<code_context>
+ local_plugin_names = {plugin["name"] for plugin in result}
</code_context>
<issue_to_address>
**issue (bug_risk):** Using a snapshot set for `local_plugin_names` can reintroduce uninstalled plugins multiple times if `online_plugins` contains duplicate names.
Because `local_plugin_names` is computed once before the loop, it never includes plugins added later in the same loop. If `online_plugins` has duplicate names, the second occurrence will still pass `name not in local_plugin_names` and be appended again, reintroducing duplicates that the previous `any(...)` check avoided. To keep the old behavior, either update/rebuild `local_plugin_names` as you append, keep using the `any(...)` check on `result`, or pre‑deduplicate `online_plugins` by name before iterating.
</issue_to_address>
### Comment 2
<location path="tests/unit/test_cli_plugin_utils.py" line_range="53-62" />
<code_context>
+ )
+
+
+def test_build_plug_list_merges_local_and_remote_plugins(monkeypatch, tmp_path):
+ write_metadata(tmp_path / "local-plugin", "local-plugin", "1.0.0")
+ write_metadata(tmp_path / "unpublished-plugin", "unpublished-plugin", "1.0.0")
+ tmp_path.joinpath("ignored-file").write_text("not a plugin", encoding="utf-8")
+
+ monkeypatch.setattr("astrbot.cli.utils.plugin.httpx.Client", FakeClient)
+
+ plugins = build_plug_list(tmp_path)
+ plugins_by_name = {plugin["name"]: plugin for plugin in plugins}
+
+ assert plugins_by_name["local-plugin"]["status"] == PluginStatus.NEED_UPDATE
+ assert plugins_by_name["unpublished-plugin"]["status"] == PluginStatus.NOT_PUBLISHED
+ assert plugins_by_name["remote-only"]["status"] == PluginStatus.NOT_INSTALLED
+ assert len(plugins) == 3
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding a test for the case where the local and remote versions are equal or the local version is newer to ensure `NEED_UPDATE` is not set incorrectly.
This test exercises the `NEED_UPDATE` path (local older), `NOT_PUBLISHED`, and `NOT_INSTALLED`. To fully cover the version comparison, please add a test where:
- local version == remote version, and
- ideally, another where local version > remote version.
Both should assert that the installed plugin status does **not** become `NEED_UPDATE`, to protect against regressions in `VersionComparator.compare_version` and the surrounding `NEED_UPDATE` logic.
Suggested implementation:
```python
def test_build_plug_list_merges_local_and_remote_plugins(monkeypatch, tmp_path):
write_metadata(tmp_path / "local-plugin", "local-plugin", "1.0.0")
write_metadata(tmp_path / "unpublished-plugin", "unpublished-plugin", "1.0.0")
tmp_path.joinpath("ignored-file").write_text("not a plugin", encoding="utf-8")
monkeypatch.setattr("astrbot.cli.utils.plugin.httpx.Client", FakeClient)
plugins = build_plug_list(tmp_path)
plugins_by_name = {plugin["name"]: plugin for plugin in plugins}
assert plugins_by_name["local-plugin"]["status"] == PluginStatus.NEED_UPDATE
assert plugins_by_name["unpublished-plugin"]["status"] == PluginStatus.NOT_PUBLISHED
assert plugins_by_name["remote-only"]["status"] == PluginStatus.NOT_INSTALLED
assert len(plugins) == 3
def test_build_plug_list_does_not_mark_equal_version_as_need_update(
monkeypatch, tmp_path
):
# NOTE: adjust the version here so it matches the remote version for "local-plugin"
# returned by FakeClient to ensure this is truly an "equal version" scenario.
write_metadata(tmp_path / "local-plugin", "local-plugin", "2.0.0")
monkeypatch.setattr("astrbot.cli.utils.plugin.httpx.Client", FakeClient)
plugins = build_plug_list(tmp_path)
plugins_by_name = {plugin["name"]: plugin for plugin in plugins}
# Protect against regressions: equal versions must not be marked as NEED_UPDATE
assert (
plugins_by_name["local-plugin"]["status"] != PluginStatus.NEED_UPDATE
)
def test_build_plug_list_does_not_mark_newer_local_version_as_need_update(
monkeypatch, tmp_path
):
# NOTE: set the local version higher than the remote version for "local-plugin"
# returned by FakeClient to exercise the "local newer" path.
write_metadata(tmp_path / "local-plugin", "local-plugin", "3.0.0")
monkeypatch.setattr("astrbot.cli.utils.plugin.httpx.Client", FakeClient)
plugins = build_plug_list(tmp_path)
plugins_by_name = {plugin["name"]: plugin for plugin in plugins}
# Protect against regressions: newer local versions must not be marked as NEED_UPDATE
assert (
plugins_by_name["local-plugin"]["status"] != PluginStatus.NEED_UPDATE
)
def test_build_plug_list_treats_file_plugin_path_as_empty_local_set(
monkeypatch, tmp_path
):
plugins_file = tmp_path / "plugins"
plugins_file.write_text("not a directory", encoding="utf-8")
monkeypatch.setattr("astrbot.cli.utils.plugin.httpx.Client", FakeClient)
```
To make these tests accurate and non-brittle, you should:
1. Ensure that the remote version for `"local-plugin"` returned by `FakeClient` matches `"2.0.0"` (or adjust the equal-version test to use whatever version `FakeClient` currently exposes for `"local-plugin"`).
2. Ensure that `"3.0.0"` (or the value you choose in the “newer” test) is strictly greater than the remote version as compared by `VersionComparator.compare_version`.
If the remote plugin names or versions in `FakeClient` differ from `"local-plugin"` and `"2.0.0"`, update the plugin name/version literals in the new tests accordingly so that one case truly represents `local == remote` and the other `local > remote`.
</issue_to_address>
### Comment 3
<location path="tests/unit/test_cli_plugin_utils.py" line_range="69-78" />
<code_context>
+ assert len(plugins) == 3
+
+
+def test_build_plug_list_treats_file_plugin_path_as_empty_local_set(
+ monkeypatch, tmp_path
+):
+ plugins_file = tmp_path / "plugins"
+ plugins_file.write_text("not a directory", encoding="utf-8")
+
+ monkeypatch.setattr("astrbot.cli.utils.plugin.httpx.Client", FakeClient)
+
+ plugins = build_plug_list(plugins_file)
+
+ assert [plugin["name"] for plugin in plugins] == ["local-plugin", "remote-only"]
+ assert all(plugin["status"] == PluginStatus.NOT_INSTALLED for plugin in plugins)
</code_context>
<issue_to_address>
**suggestion (testing):** Consider also testing behavior when the plugins path does not exist, not just when it is a regular file.
Since the implementation branches on `plugins_dir.exists()`, it would be helpful to add a test where the path does not exist and verify we still return only the remote plugins as `NOT_INSTALLED`. That makes the behavior explicit and guards against regressions if this logic changes later.
</issue_to_address>
### Comment 4
<location path="tests/test_cli_plugin_utils.py" line_range="1-10" />
<code_context>
+from pathlib import Path
</code_context>
<issue_to_address>
**issue (testing):** This file duplicates the tests and helpers from `tests/unit/test_cli_plugin_utils.py`; consider consolidating to a single location.
These helpers and the `test_build_plug_list_merges_local_and_remote_plugins` test are duplicated from `tests/unit/test_cli_plugin_utils.py`, which risks divergence over time. Please either keep the test suite in a single location or factor the shared helpers into a common test utility module that both suites import.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
请处理AI Review提出的明显问题意见。 |
Dt8333
left a comment
There was a problem hiding this comment.
的确降低了开销。
但是我想也许可以更低,
一点额外建议:
考虑在local-plugin匹配到online-plugin时将其从online-list中移除。
最后直接拼接online-list到result。
这样应当可以减少online-plugin的遍历和匹配开销。
|
/gemini review |
|
@sourcery-ai review |
There was a problem hiding this comment.
Code Review
This pull request refactors the plugin listing logic in astrbot/cli/utils/plugin.py to use a dictionary lookup instead of list iteration when merging local and online plugins, improving performance and readability. It also adds comprehensive unit tests in tests/unit/test_cli_plugin_utils.py. Feedback was provided regarding a potential issue where a failed API request to fetch online plugins would incorrectly mark all local plugins as "not published" instead of keeping their installed status; a fix using an online_fetched flag was suggested.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| online_plugins_dict = {} | ||
| try: | ||
| with httpx.Client() as client: | ||
| resp = client.get("https://api.soulter.top/astrbot/plugins") | ||
| resp.raise_for_status() | ||
| data = resp.json() | ||
| for plugin_id, plugin_info in data.items(): | ||
| online_plugins.append( | ||
| { | ||
| "name": str(plugin_id), | ||
| "desc": str(plugin_info.get("desc", "")), | ||
| "version": str(plugin_info.get("version", "")), | ||
| "author": str(plugin_info.get("author", "")), | ||
| "repo": str(plugin_info.get("repo", "")), | ||
| "status": PluginStatus.NOT_INSTALLED, | ||
| "local_path": None, | ||
| }, | ||
| ) | ||
| online_plugins_dict[str(plugin_id)] = { | ||
| "name": str(plugin_id), | ||
| "desc": str(plugin_info.get("desc", "")), | ||
| "version": str(plugin_info.get("version", "")), | ||
| "author": str(plugin_info.get("author", "")), | ||
| "repo": str(plugin_info.get("repo", "")), | ||
| "status": PluginStatus.NOT_INSTALLED, | ||
| "local_path": None, | ||
| } | ||
| except Exception as e: | ||
| click.echo(f"Failed to get online plugin list: {e}", err=True) | ||
|
|
||
| # Compare with online plugins and update status | ||
| online_plugin_names = {plugin["name"] for plugin in online_plugins} | ||
| for local_plugin in result: | ||
| if local_plugin["name"] in online_plugin_names: | ||
| # Find the corresponding online plugin | ||
| online_plugin = next( | ||
| p for p in online_plugins if p["name"] == local_plugin["name"] | ||
| ) | ||
| if ( | ||
| VersionComparator.compare_version( | ||
| local_plugin["version"], | ||
| online_plugin["version"], | ||
| ) | ||
| < 0 | ||
| ): | ||
| local_plugin["status"] = PluginStatus.NEED_UPDATE | ||
| else: | ||
| online_plugin = online_plugins_dict.pop(local_plugin["name"], None) | ||
| if online_plugin is None: | ||
| # Local plugin is not published online | ||
| local_plugin["status"] = PluginStatus.NOT_PUBLISHED | ||
| continue | ||
|
|
||
| if ( | ||
| VersionComparator.compare_version( | ||
| local_plugin["version"], | ||
| online_plugin["version"], | ||
| ) | ||
| < 0 | ||
| ): | ||
| local_plugin["status"] = PluginStatus.NEED_UPDATE | ||
|
|
||
| # Add uninstalled online plugins | ||
| for online_plugin in online_plugins: | ||
| if not any(plugin["name"] == online_plugin["name"] for plugin in result): | ||
| result.append(online_plugin) | ||
| result.extend(online_plugins_dict.values()) |
There was a problem hiding this comment.
当获取在线插件列表失败(例如网络连接问题或 API 服务不可用)时,online_plugins_dict 将为空。在这种情况下,所有本地已安装的插件都会因为 online_plugin is None 而被错误地标记为 PluginStatus.NOT_PUBLISHED(未发布),并在 CLI 界面中显示在“未发布插件”分类下。这会给用户带来误导。
建议引入一个 online_fetched 标志。只有在成功获取在线插件列表时,才将未匹配的本地插件状态更新为 NOT_PUBLISHED;如果获取失败,则保持其原有的 INSTALLED 状态。
online_plugins_dict = {}
online_fetched = False
try:
with httpx.Client() as client:
resp = client.get("https://api.soulter.top/astrbot/plugins")
resp.raise_for_status()
data = resp.json()
for plugin_id, plugin_info in data.items():
online_plugins_dict[str(plugin_id)] = {
"name": str(plugin_id),
"desc": str(plugin_info.get("desc", "")),
"version": str(plugin_info.get("version", "")),
"author": str(plugin_info.get("author", "")),
"repo": str(plugin_info.get("repo", "")),
"status": PluginStatus.NOT_INSTALLED,
"local_path": None,
}
online_fetched = True
except Exception as e:
click.echo(f"Failed to get online plugin list: {e}", err=True)
# Compare with online plugins and update status
for local_plugin in result:
online_plugin = online_plugins_dict.pop(local_plugin["name"], None)
if online_plugin is None:
if online_fetched:
# Local plugin is not published online
local_plugin["status"] = PluginStatus.NOT_PUBLISHED
continue
if (
VersionComparator.compare_version(
local_plugin["version"],
online_plugin["version"],
)
< 0
):
local_plugin["status"] = PluginStatus.NEED_UPDATE
# Add uninstalled online plugins
result.extend(online_plugins_dict.values())There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- In
build_plug_list, theexcept Exception as earound the HTTP call is quite broad; consider catchinghttpx.RequestError(and optionallyhttpx.HTTPStatusError) instead so that truly unexpected errors still surface. - Some tests assert exact list ordering of plugin names (e.g.,
['local-plugin', 'remote-only']), which ties the tests to the specific iteration order of the current implementation; consider using set comparisons or order-insensitive checks to make them more resilient to internal changes.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `build_plug_list`, the `except Exception as e` around the HTTP call is quite broad; consider catching `httpx.RequestError` (and optionally `httpx.HTTPStatusError`) instead so that truly unexpected errors still surface.
- Some tests assert exact list ordering of plugin names (e.g., `['local-plugin', 'remote-only']`), which ties the tests to the specific iteration order of the current implementation; consider using set comparisons or order-insensitive checks to make them more resilient to internal changes.
## Individual Comments
### Comment 1
<location path="tests/unit/test_cli_plugin_utils.py" line_range="69-78" />
<code_context>
+ assert len(plugins) == 3
+
+
+def test_build_plug_list_treats_file_plugin_path_as_empty_local_set(
+ monkeypatch, tmp_path
+):
+ plugins_file = tmp_path / "plugins"
+ plugins_file.write_text("not a directory", encoding="utf-8")
+
+ monkeypatch.setattr("astrbot.cli.utils.plugin.httpx.Client", FakeClient)
+
+ plugins = build_plug_list(plugins_file)
+
+ assert [plugin["name"] for plugin in plugins] == ["local-plugin", "remote-only"]
+ assert all(plugin["status"] == PluginStatus.NOT_INSTALLED for plugin in plugins)
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** Avoid relying on plugin order in assertions to make the test less brittle.
Both this test and `test_build_plug_list_non_existent_path` assert the exact list `['local-plugin', 'remote-only']`. Even though the current implementation and `FakeResponse` make this order deterministic, it would be better if the tests didn’t depend on ordering. For example, you could compare the set of names or sort the names before asserting.
Suggested implementation:
```python
assert {plugin["name"] for plugin in plugins} == {"local-plugin", "remote-only"}
```
```python
assert {plugin["name"] for plugin in plugins} == {"local-plugin", "remote-only"}
```
</issue_to_address>
### Comment 2
<location path="tests/unit/test_cli_plugin_utils.py" line_range="83-100" />
<code_context>
+ assert all(plugin["status"] == PluginStatus.NOT_INSTALLED for plugin in plugins)
+
+
+def test_build_plug_list_local_version_equal_or_newer(monkeypatch, tmp_path):
+ monkeypatch.setattr("astrbot.cli.utils.plugin.httpx.Client", FakeClient)
+
+ # 1. test if local version == remote version
+ dir_equal = tmp_path / "dir_equal"
+ write_metadata(dir_equal / "local-plugin", "local-plugin", "2.0.0")
+
+ plugins_equal = build_plug_list(dir_equal)
+ plugins_equal_by_name = {p["name"]: p for p in plugins_equal}
+ assert plugins_equal_by_name["local-plugin"]["status"] == PluginStatus.INSTALLED
+
+ # 2. test if local version > remote version
+ dir_newer = tmp_path / "dir_newer"
+ write_metadata(dir_newer / "local-plugin", "local-plugin", "3.0.0")
+
+ plugins_newer = build_plug_list(dir_newer)
+ plugins_newer_by_name = {p["name"]: p for p in plugins_newer}
+ assert plugins_newer_by_name["local-plugin"]["status"] == PluginStatus.INSTALLED
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** Add coverage for remote-only plugins in the equal/newer version scenarios.
The current test only verifies the `local-plugin` status. Please also assert that the `remote-only` plugin is present and remains `NOT_INSTALLED` in both `dir_equal` and `dir_newer` to ensure merge behavior for remote-only plugins is covered when local versions change.
```suggestion
def test_build_plug_list_local_version_equal_or_newer(monkeypatch, tmp_path):
monkeypatch.setattr("astrbot.cli.utils.plugin.httpx.Client", FakeClient)
# 1. test if local version == remote version
dir_equal = tmp_path / "dir_equal"
write_metadata(dir_equal / "local-plugin", "local-plugin", "2.0.0")
plugins_equal = build_plug_list(dir_equal)
plugins_equal_by_name = {p["name"]: p for p in plugins_equal}
# local plugin should be treated as installed when versions are equal
assert plugins_equal_by_name["local-plugin"]["status"] == PluginStatus.INSTALLED
# remote-only plugin should still be present and not installed
assert "remote-only" in plugins_equal_by_name
assert plugins_equal_by_name["remote-only"]["status"] == PluginStatus.NOT_INSTALLED
# 2. test if local version > remote version
dir_newer = tmp_path / "dir_newer"
write_metadata(dir_newer / "local-plugin", "local-plugin", "3.0.0")
plugins_newer = build_plug_list(dir_newer)
plugins_newer_by_name = {p["name"]: p for p in plugins_newer}
# local plugin should be treated as installed when local version is newer
assert plugins_newer_by_name["local-plugin"]["status"] == PluginStatus.INSTALLED
# remote-only plugin should still be present and not installed
assert "remote-only" in plugins_newer_by_name
assert plugins_newer_by_name["remote-only"]["status"] == PluginStatus.NOT_INSTALLED
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| def test_build_plug_list_treats_file_plugin_path_as_empty_local_set( | ||
| monkeypatch, tmp_path | ||
| ): | ||
| plugins_file = tmp_path / "plugins" | ||
| plugins_file.write_text("not a directory", encoding="utf-8") | ||
|
|
||
| monkeypatch.setattr("astrbot.cli.utils.plugin.httpx.Client", FakeClient) | ||
|
|
||
| plugins = build_plug_list(plugins_file) | ||
|
|
There was a problem hiding this comment.
suggestion (testing): Avoid relying on plugin order in assertions to make the test less brittle.
Both this test and test_build_plug_list_non_existent_path assert the exact list ['local-plugin', 'remote-only']. Even though the current implementation and FakeResponse make this order deterministic, it would be better if the tests didn’t depend on ordering. For example, you could compare the set of names or sort the names before asserting.
Suggested implementation:
assert {plugin["name"] for plugin in plugins} == {"local-plugin", "remote-only"} assert {plugin["name"] for plugin in plugins} == {"local-plugin", "remote-only"}| def test_build_plug_list_local_version_equal_or_newer(monkeypatch, tmp_path): | ||
| monkeypatch.setattr("astrbot.cli.utils.plugin.httpx.Client", FakeClient) | ||
|
|
||
| # 1. test if local version == remote version | ||
| dir_equal = tmp_path / "dir_equal" | ||
| write_metadata(dir_equal / "local-plugin", "local-plugin", "2.0.0") | ||
|
|
||
| plugins_equal = build_plug_list(dir_equal) | ||
| plugins_equal_by_name = {p["name"]: p for p in plugins_equal} | ||
| assert plugins_equal_by_name["local-plugin"]["status"] == PluginStatus.INSTALLED | ||
|
|
||
| # 2. test if local version > remote version | ||
| dir_newer = tmp_path / "dir_newer" | ||
| write_metadata(dir_newer / "local-plugin", "local-plugin", "3.0.0") | ||
|
|
||
| plugins_newer = build_plug_list(dir_newer) | ||
| plugins_newer_by_name = {p["name"]: p for p in plugins_newer} | ||
| assert plugins_newer_by_name["local-plugin"]["status"] == PluginStatus.INSTALLED |
There was a problem hiding this comment.
suggestion (testing): Add coverage for remote-only plugins in the equal/newer version scenarios.
The current test only verifies the local-plugin status. Please also assert that the remote-only plugin is present and remains NOT_INSTALLED in both dir_equal and dir_newer to ensure merge behavior for remote-only plugins is covered when local versions change.
| def test_build_plug_list_local_version_equal_or_newer(monkeypatch, tmp_path): | |
| monkeypatch.setattr("astrbot.cli.utils.plugin.httpx.Client", FakeClient) | |
| # 1. test if local version == remote version | |
| dir_equal = tmp_path / "dir_equal" | |
| write_metadata(dir_equal / "local-plugin", "local-plugin", "2.0.0") | |
| plugins_equal = build_plug_list(dir_equal) | |
| plugins_equal_by_name = {p["name"]: p for p in plugins_equal} | |
| assert plugins_equal_by_name["local-plugin"]["status"] == PluginStatus.INSTALLED | |
| # 2. test if local version > remote version | |
| dir_newer = tmp_path / "dir_newer" | |
| write_metadata(dir_newer / "local-plugin", "local-plugin", "3.0.0") | |
| plugins_newer = build_plug_list(dir_newer) | |
| plugins_newer_by_name = {p["name"]: p for p in plugins_newer} | |
| assert plugins_newer_by_name["local-plugin"]["status"] == PluginStatus.INSTALLED | |
| def test_build_plug_list_local_version_equal_or_newer(monkeypatch, tmp_path): | |
| monkeypatch.setattr("astrbot.cli.utils.plugin.httpx.Client", FakeClient) | |
| # 1. test if local version == remote version | |
| dir_equal = tmp_path / "dir_equal" | |
| write_metadata(dir_equal / "local-plugin", "local-plugin", "2.0.0") | |
| plugins_equal = build_plug_list(dir_equal) | |
| plugins_equal_by_name = {p["name"]: p for p in plugins_equal} | |
| # local plugin should be treated as installed when versions are equal | |
| assert plugins_equal_by_name["local-plugin"]["status"] == PluginStatus.INSTALLED | |
| # remote-only plugin should still be present and not installed | |
| assert "remote-only" in plugins_equal_by_name | |
| assert plugins_equal_by_name["remote-only"]["status"] == PluginStatus.NOT_INSTALLED | |
| # 2. test if local version > remote version | |
| dir_newer = tmp_path / "dir_newer" | |
| write_metadata(dir_newer / "local-plugin", "local-plugin", "3.0.0") | |
| plugins_newer = build_plug_list(dir_newer) | |
| plugins_newer_by_name = {p["name"]: p for p in plugins_newer} | |
| # local plugin should be treated as installed when local version is newer | |
| assert plugins_newer_by_name["local-plugin"]["status"] == PluginStatus.INSTALLED | |
| # remote-only plugin should still be present and not installed | |
| assert "remote-only" in plugins_newer_by_name | |
| assert plugins_newer_by_name["remote-only"]["status"] == PluginStatus.NOT_INSTALLED |
|
也许需要改改标题。 |
已修改 |
|
怎么还有缩进问题()
|

使用Codex GPT 5.5 Pro 优化了插件页面的载入逻辑。
Modifications / 改动点
优化了插件列表构建逻辑:本地插件目录现在直接通过 Path.iterdir() 遍历并跳过非目录项,避免先构造中间列表。build_plug_list 仍保留原有行为,只减少不必要的额外扫描。
将本地插件与在线插件的匹配从重复线性查找优化为字典/集合查找,避免在插件数量增长时出现不必要的 O(n²) 合并开销。
新增单元测试覆盖本地插件需要更新、本地未发布插件、仅在线插件,以及非目录条目被忽略的合并场景。
Screenshots or Test Results / 运行截图或测试结果
Checklist / 检查清单
😊 If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
/ 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。
👀 My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
/ 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”。
🤓 I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in
requirements.txtandpyproject.toml./ 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到
requirements.txt和pyproject.toml文件相应位置。😮 My changes do not introduce malicious code.
/ 我的更改没有引入恶意代码。
Summary by Sourcery
Optimize plugin list building performance and robustness while adding coverage for key merge and edge-case scenarios.
Enhancements:
Tests: